John von Neumann, 1945
On peut facilement fusionner deux listes triées en une seule en en extrayant itérativement le plus petit élément. Celui-ci est forcément aussi le plus petit de l'une des deux listes à fusionner.
Ce procédé est appelé fusion et est au cœur de l'algorithme de tri par fusion récursif.
Ce tri a été illustré par Saturday Morning Breakfast Cereal
si T1 est vide, min(T2) sinon, si T2 est vide, min(T1) sinon, le plus petit de min(T1),min(T2) Les listes Tk (T1 et T2) étant triées,
ik est l'indice du premier élément pas encore fusionné min(Tk) est Tk[ik] ik += 1 supprime ce minimum de Tk.def fusion(T, premier, limite, dernier):
asd1.affiche_entree_fusion(T,premier, limite, dernier)
T1 = T[premier:limite].copy()
T2 = T[limite:dernier].copy()
i1 = i2 = 0
for i in range(premier,dernier):
if i2 < len(T2) and ( i1 >= len(T1) or
T2[i2] < T1[i1]):
T[i] = T2[i2]; i2 += 1
else:
T[i] = T1[i1]; i1 += 1
asd1.affiche_sortie_fusion(T,premier,limite,dernier)
T = [ 3, 4, 5, 1, 2, 6 ]; fusion( T, 0, 3, 6 )
[3, 4, 5][1, 2, 6] F(0,3,6) [1, 2, 3, 4, 5, 6]
def recursion(T,premier,dernier):
asd1.affiche_entree_tri_fusion(T,premier, dernier)
N = dernier - premier
if N >= 2:
milieu = premier + N//2
recursion(T,premier,milieu)
recursion(T,milieu,dernier)
fusion(T,premier,milieu,dernier)
def tri(T):
recursion(T,0,len(T))
T = [5, 4, 3, 2, 6, 7, 1]; tri(T)
[5, 4, 3, 2, 6, 7, 1] R(0,7) [5, 4, 3]............ R(0,3) [5].................. R(0,1) ...[4, 3]............ R(1,3) ...[4]............... R(1,2) ......[3]............ R(2,3) ...[4][3]............ F(1,2,3) ...[3, 4]............ [5][3, 4]............ F(0,1,3) [3, 4, 5]............ .........[2, 6, 7, 1] R(3,7) .........[2, 6]...... R(3,5) .........[2]......... R(3,4) ............[6]...... R(4,5) .........[2][6]...... F(3,4,5) .........[2, 6]...... ...............[7, 1] R(5,7) ...............[7]... R(5,6) ..................[1] R(6,7) ...............[7][1] F(5,6,7) ...............[1, 7] .........[2, 6][1, 7] F(3,5,7) .........[1, 2, 6, 7] [3, 4, 5][1, 2, 6, 7] F(0,3,7) [1, 2, 3, 4, 5, 6, 7]
def fusionner(T, premier, milieu, dernier,
comparer = asd1.plus_petit):
T1 = asd1.copier_tableau(T[premier:milieu]); i1 = 0
T2 = asd1.copier_tableau(T[milieu:dernier]); i2 = 0
for i in range(premier,dernier):
if i2 < len(T2) and ( i1 >= len(T1) or
comparer(T2[i2],T1[i1])):
T[i] = asd1.assigner(T2[i2]); i2 += 1;
else:
T[i] = asd1.assigner(T1[i1]); i1 += 1;
def tri_fusion_recursif(T,premier,dernier,
comparer = asd1.plus_petit):
if dernier - premier >= 2:
milieu = premier + (dernier - premier) // 2
tri_fusion_recursif(T, premier, milieu, comparer)
tri_fusion_recursif(T, milieu, dernier, comparer)
fusionner(T,premier,milieu,dernier,comparer)
def tri_fusion(T, comparer = asd1.plus_petit ):
tri_fusion_recursif(T,0,len(T),comparer)
Trions un tableau de 64 entiers aléatoires entre 0 et 100. Nous affichons l'état du tableau aprés les étapes de fusion qui fusionnent 16 éléments ou plus.
import numpy as np
T = np.random.randint(0,100,64)
asd1.afficheIteration(T,'Tableau original')
tri_fusion_recursif(T,0,16)
asd1.afficheIteration(T,'Après recursion(0,16)')
tri_fusion_recursif(T,16,32)
asd1.afficheIteration(T,'Après recursion(16,32)')
fusionner(T,0,16,32)
asd1.afficheIteration(T,'Après fusion(0,16,32)')
tri_fusion_recursif(T,32,48)
asd1.afficheIteration(T,'Après recursion(32,48)')
tri_fusion_recursif(T,48,64)
asd1.afficheIteration(T,'Après recursion(48,65)')
fusionner(T,32,48,64)
asd1.afficheIteration(T,'Après fusion(0,16,32)')
fusionner(T,0,32,64)
asd1.afficheIteration(T,'Après fusion(0,32,64)')
Le tri fusion est stable.
La ligne critique est le test T2[i2] < T1[i1].
En cas d'égalité entre l'élément le plus petit de T1 ou de T2, il faut d'abord copier dans T celui de T1. En effet, il vient de la section [premier:milieu] qui est antérieure à la section [milieu:dernier]
Vérifions le en triant par parties fractionnaires puis par parties entières.
asd1.test_stabilite(tri_fusion)
Le tri est stable
Evaluons d'abord la complexité du tri d'un tableau au contenu généré aléatoirement.
asd1.evalue_complexite(tri_fusion, asd1.tableau_aleatoire,
"tri fusion, tableau aléatoire")
La complexité du tri est linéarithmique en $\Theta(n\log(n))$.
Par ailleurs, le nombre d'opérations est indépendant du contenu de l'entrée. Il n'y a pas de meilleur ou de pire cas.
S'il est efficace pour le nombre de comparaisons, ce tri effectue un très grand nombre d'écritures dans le tableau. A chaque fusion, chaque élément est en effet copié 2 fois.
Il est possible d'éviter la première de ces copies en utilisant toujours le même tableau annexe de la taille du tableau T original, et en échangeant le rôle des deux tableaux à chaque niveau de récursion
La fonction de fusion prend 2 tableaux en paramètres: IN et OUT
def fusionner2(OUT, IN, premier, milieu, dernier,
comparer = asd1.plus_petit):
i1 = premier; i2 = milieu
for i in range(premier,dernier):
if i2 < dernier and (
i1 >= milieu or comparer(IN[i2],IN[i1]) ):
OUT[i] = asd1.assigner(IN[i2]); i2 += 1;
else:
OUT[i] = asd1.assigner(IN[i1]); i1 += 1;
La fonction récursive prend les même deux tableaux en paramètre. Les appels récursifs en échangent le rôle.
def tri_fusion_recursif2(OUT,IN,premier,dernier,comparer = asd1.plus_petit):
if dernier - premier >= 2:
milieu = premier + int((dernier - premier)/2)
tri_fusion_recursif2(IN,OUT, premier, milieu, comparer)
tri_fusion_recursif2(IN,OUT, milieu, dernier, comparer)
fusionner2(OUT,IN,premier,milieu,dernier,comparer)
La fonction d'appel originale crée le tableau annexe en copiant le tableau original.
def tri_fusion2(T, comparer = asd1.plus_petit ):
TMP = asd1.copier_tableau(T)
tri_fusion_recursif2(T,TMP,0,len(T),comparer)
Toutes les copies T → T1 et T → T2 sont remplacées par une seule copie de T → TMP. On passe donc d'environ $2 \cdot n \cdot \log n$ écritures à seulement $n \cdot \log n + n$.
asd1.evalue_complexite(tri_fusion2, asd1.tableau_aleatoire,
"tri fusion, tableau aléatoire")
Les deux versions de l'algorithme présentées demandent de copier tous les éléments dans un tableau annexe. La mémoire additionelle utilisée est donc $\Theta(n)$.
Si ce n'est pas acceptable, il existe des alternatives
std::stable_sort en C++ quand la mémoire est limitée. Le tri fusion
est stable
a une complexité temporelle linéarithmique, en $\Theta(n \log n)$
a une complexité spatiale linéaire, en $\Theta(n)$
a des variantes moins gourmandes en mémoire mais plus difficiles à coder.
|
|
© Olivier Cuisenaire, 2018 |